Skip to content

fix: handle 1D interpolation result in resample for single-state ODEs - #1

Closed
yasumorishima wants to merge 4 commits into
mainfrom
fix/resample-single-state
Closed

fix: handle 1D interpolation result in resample for single-state ODEs#1
yasumorishima wants to merge 4 commits into
mainfrom
fix/resample-single-state

Conversation

@yasumorishima

@yasumorishima yasumorishima commented Mar 19, 2026

Copy link
Copy Markdown
Owner

Summary

Fix TrajectoryAnalysis.resample failing for ODESystems with a single state variable.

Fixes nasa#70

Summary by CodeRabbit

  • Bug Fixes

    • Ensured interpolated trajectory segments produced during resampling always have a consistent shape, preventing shape-related errors in subsequent per-time dynamic evaluations.
  • Tests

    • Added tests for resampling on single- and multi-state systems covering time-grid generation, output inclusion options, numerical accuracy, and resolution stability.

When an ODESystem has only a single state variable, scipy interpolation
returns a 1D array (n,) instead of 2D (n, 1). This causes a broadcast
error when assigning to the pre-allocated 2D xs array.

Fix by checking ndim and adding a column axis when needed.

Fixes nasa#70
@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Intercepts interpolation output in TrajectoryAnalysis.resample, reshapes 1D results to 2D before assigning into the preallocated xs slice to prevent shape-mismatch for single-state ODEs. Adds tests covering single-state and multi-state resampling behaviors.

Changes

Cohort / File(s) Summary
Resample fix
src/condor/contrib.py
Capture interpolation into interp_result; if interp_result.ndim == 1 reshape to 2D via interp_result[:, np.newaxis] before assigning to xs[idx0:idx1], preventing broadcast errors for single-state ODEs.
Tests added
tests/test_trajectory_analysis.py
Added TestResampleSingleState tests: verify non-empty resampled time grid, analytic match for exponential decay ODE, behavior with include_output=False/True (dynamic output reshape), resolution comparison, and a multi-state harmonic-oscillator regression case.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 I nibbled at a lonely line,
made one into two — now all align.
Interp hums smooth, no shape askew,
the trajectory hops, straight and true. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: handling 1D interpolation results in resample for single-state ODEs, which directly addresses the bug reported in issue #70.
Linked Issues check ✅ Passed The code changes directly implement the objective from issue #70: detecting 1D interpolation results and reshaping them to 2D before assignment, which resolves the broadcasting ValueError.
Out of Scope Changes check ✅ Passed All changes are scope to the fix: production code handles 1D interpolation reshaping, and test additions provide comprehensive coverage for single-state and multi-state ODE resampling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/resample-single-state
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@yasumorishima

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yasumorishima

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@yasumorishima

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@yasumorishima

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

1 similar comment
@yasumorishima

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

6 tests covering:
- Basic single-state resample
- Resampled values match analytical solution (exponential decay)
- include_output=False path
- dynamic_output with single-state ODE
- Different dt values
- Multi-state ODE regression (harmonic oscillator)

Also discovered that include_events=True triggers a separate IndexError
(issue nasa#71, t_size calculation off-by-2) — tests use include_events=False
to isolate the ndim fix from that pre-existing bug.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/test_trajectory_analysis.py (2)

388-463: Consider DRYing repeated single-state ODE definitions.

The same ODE/Trajectory scaffolding is repeated across several tests (Line 390–Line 413, Line 420–Line 428, Line 452–Line 460). A small helper would reduce noise and make intent clearer.

Example refactor direction
 class TestResampleSingleState:
+    `@staticmethod`
+    def _make_single_state_traj(with_output=False):
+        class ODE(co.ODESystem):
+            a = parameter()
+            x = state()
+            if with_output:
+                dynamic_output.velocity = -a * x
+            dot[x] = -a * x
+
+        class Traj(ODE.TrajectoryAnalysis):
+            tf = 10
+            initial[x] = 1
+            if with_output:
+                vel = dynamic_output.velocity
+
+        return Traj
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_trajectory_analysis.py` around lines 388 - 463, Multiple tests
duplicate the same single-state ODE and TrajectoryAnalysis scaffolding (classes
named ODE and Traj) — create a small helper to return a ready-to-use trajectory
instance and use it in each test instead of repeating the class definitions.
Implement a factory function (e.g., make_single_state_traj(a=0.5, tf=10,
initial=1, dynamic_output=False)) that builds the ODE/Traj classes (including
optional dynamic_output and exposing vel when needed) and returns an
instantiated sim; then update test_resample_single_state*,
test_resample_single_state_values, test_resample_single_state_no_output,
test_resample_single_state_with_dynamic_output, and
test_resample_single_state_small_dt to call make_single_state_traj(...) and call
sim.resample(...) as before. Ensure the helper preserves parameter names and
initial state behavior so existing assertions against resampled.t and
resampled.x continue to work.

433-449: Assert dynamic output values, not only successful execution.

At Line 447–Line 448, the test proves the call doesn’t crash, but it won’t catch incorrect dynamic_output values/shaping. Add a value assertion on resampled.vel so this path is actually validated.

Proposed test strengthening
     sim = Traj(a=0.5)
     resampled = sim.resample(1.0, include_events=False, include_output=True)
     assert resampled.t.size > 0
+    expected_vel = -0.5 * np.exp(-0.5 * resampled.t)
+    np.testing.assert_allclose(resampled.vel, expected_vel, rtol=1e-4)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_trajectory_analysis.py` around lines 433 - 449, The test currently
only ensures resample runs; add an assertion that dynamic_output values are
correct by comparing resampled.vel against the analytical velocity -a * exp(-a *
t) (or elementwise -a * x where x = exp(-a * t)) from the Traj/Timestep results;
use the same a passed to Traj(a=0.5) and compare elementwise across resampled.t
(or at the t=1.0 sample) with an approximate-equality check (e.g.,
allclose/approx) to validate both value and shaping for dynamic_output.velocity
in test_resample_single_state_with_dynamic_output.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/test_trajectory_analysis.py`:
- Around line 388-463: Multiple tests duplicate the same single-state ODE and
TrajectoryAnalysis scaffolding (classes named ODE and Traj) — create a small
helper to return a ready-to-use trajectory instance and use it in each test
instead of repeating the class definitions. Implement a factory function (e.g.,
make_single_state_traj(a=0.5, tf=10, initial=1, dynamic_output=False)) that
builds the ODE/Traj classes (including optional dynamic_output and exposing vel
when needed) and returns an instantiated sim; then update
test_resample_single_state*, test_resample_single_state_values,
test_resample_single_state_no_output,
test_resample_single_state_with_dynamic_output, and
test_resample_single_state_small_dt to call make_single_state_traj(...) and call
sim.resample(...) as before. Ensure the helper preserves parameter names and
initial state behavior so existing assertions against resampled.t and
resampled.x continue to work.
- Around line 433-449: The test currently only ensures resample runs; add an
assertion that dynamic_output values are correct by comparing resampled.vel
against the analytical velocity -a * exp(-a * t) (or elementwise -a * x where x
= exp(-a * t)) from the Traj/Timestep results; use the same a passed to
Traj(a=0.5) and compare elementwise across resampled.t (or at the t=1.0 sample)
with an approximate-equality check (e.g., allclose/approx) to validate both
value and shaping for dynamic_output.velocity in
test_resample_single_state_with_dynamic_output.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af635ab5-2dd0-474e-a3ff-1d4d07754070

📥 Commits

Reviewing files that changed from the base of the PR and between 5a9745e and 8f706bf.

📒 Files selected for processing (1)
  • tests/test_trajectory_analysis.py

Address CodeRabbit review: assert resampled.velocity values match
analytical solution (-0.5 * exp(-0.5 * t)), not just successful execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_trajectory_analysis.py (1)

388-483: Consider extracting a shared helper/fixture for ODE/Traj setup.

The repeated class definitions are very similar across tests; a small fixture would reduce duplication and future maintenance overhead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_trajectory_analysis.py` around lines 388 - 483, Several tests
repeat near-identical ODE and Traj classes (e.g., ODE and Traj in
test_resample_single_state, test_resample_single_state_values,
test_resample_single_state_with_dynamic_output, etc.); extract a small helper or
pytest fixture like make_sim(...) that builds and returns the Traj simulator
instance (or the Traj class) configured by flags: states (['x'] or ['x','v']),
parameter a, tf, initial values, and an optional dynamic_output flag to attach
dynamic_output.velocity; replace direct class definitions in each test with
calls to make_sim(a=0.5, tf=10, initial={'x':1}, dynamic_output=True/False) so
tests reuse the factory and reduce duplication while keeping names ODE and Traj
references inside the helper for clarity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_trajectory_analysis.py`:
- Around line 419-431: The test test_resample_single_state_no_output currently
passes include_output=False but the ODE class has no dynamic_output, so the
include_output=False branch in resample isn't exercised; modify the test's ODE
(the ODE class used by Traj) to declare at least one dynamic_output (use the
dynamic_output symbol) so that when you call Traj(...).resample(...,
include_output=False, include_events=False) the code path that checks
model.dynamic_output._count and skips building outputs is actually exercised;
ensure the Traj initial/parameter setup remains the same and keep the assertion
on resampled.t.size.

---

Nitpick comments:
In `@tests/test_trajectory_analysis.py`:
- Around line 388-483: Several tests repeat near-identical ODE and Traj classes
(e.g., ODE and Traj in test_resample_single_state,
test_resample_single_state_values,
test_resample_single_state_with_dynamic_output, etc.); extract a small helper or
pytest fixture like make_sim(...) that builds and returns the Traj simulator
instance (or the Traj class) configured by flags: states (['x'] or ['x','v']),
parameter a, tf, initial values, and an optional dynamic_output flag to attach
dynamic_output.velocity; replace direct class definitions in each test with
calls to make_sim(a=0.5, tf=10, initial={'x':1}, dynamic_output=True/False) so
tests reuse the factory and reduce duplication while keeping names ODE and Traj
references inside the helper for clarity.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b5f6ded6-114e-4a6c-9980-786dc4878fbd

📥 Commits

Reviewing files that changed from the base of the PR and between 8f706bf and 28b4a63.

📒 Files selected for processing (1)
  • tests/test_trajectory_analysis.py

Comment on lines +419 to +431
def test_resample_single_state_no_output(self):
class ODE(co.ODESystem):
a = parameter()
x = state()
dot[x] = -a * x

class Traj(ODE.TrajectoryAnalysis):
tf = 10
initial[x] = 1

sim = Traj(a=0.5)
resampled = sim.resample(1.0, include_events=False, include_output=False)
assert resampled.t.size > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

include_output=False path is not truly exercised in this test.

At Line 430 you pass include_output=False, but the ODE in Lines 420-423 has no dynamic_output. In src/condor/contrib.py (Lines 693-750), include_output is gated by model.dynamic_output._count, so this ends up on the same effective path as the basic single-state test.

Suggested adjustment
     def test_resample_single_state_no_output(self):
         class ODE(co.ODESystem):
             a = parameter()
             x = state()
+            dynamic_output.velocity = -a * x
             dot[x] = -a * x

         class Traj(ODE.TrajectoryAnalysis):
             tf = 10
             initial[x] = 1

         sim = Traj(a=0.5)
         resampled = sim.resample(1.0, include_events=False, include_output=False)
         assert resampled.t.size > 0
🧰 Tools
🪛 Ruff (0.15.6)

[error] 421-421: Undefined name parameter

(F821)


[error] 422-422: Undefined name state

(F821)


[error] 423-423: Undefined name dot

(F821)


[error] 427-427: Undefined name initial

(F821)


[error] 427-427: Undefined name x

(F821)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_trajectory_analysis.py` around lines 419 - 431, The test
test_resample_single_state_no_output currently passes include_output=False but
the ODE class has no dynamic_output, so the include_output=False branch in
resample isn't exercised; modify the test's ODE (the ODE class used by Traj) to
declare at least one dynamic_output (use the dynamic_output symbol) so that when
you call Traj(...).resample(..., include_output=False, include_events=False) the
code path that checks model.dynamic_output._count and skips building outputs is
actually exercised; ensure the Traj initial/parameter setup remains the same and
keep the assertion on resampled.t.size.

@yasumorishima

Copy link
Copy Markdown
Owner Author

upstream nasa#75 closed by maintainer (2026-04-12)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TrajectoryAnalysis.resample fails on ODEs with single state

1 participant